// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //

#if UNITY_EDITOR

using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerPlayableDrawer;
using Object = UnityEngine.Object;

namespace Animancer.Editor
{
    /// <summary>[Editor-Only] Draws the Inspector GUI for an <see cref="AnimancerState"/>.</summary>
    /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerStateDrawer_1
    /// 
    public class AnimancerStateDrawer<T> : AnimancerNodeDrawer<T> where T : AnimancerState
    {
        /************************************************************************************************************************/

        /// <summary>
        /// Creates a new <see cref="AnimancerStateDrawer{T}"/> to manage the Inspector GUI for the `target`.
        /// </summary>
        public AnimancerStateDrawer(T target)
            => Target = target;

        /************************************************************************************************************************/

        /// <summary>The <see cref="GUIStyle"/> used for the area encompassing this drawer is <c>null</c>.</summary>
        protected override GUIStyle RegionStyle
            => null;

        /************************************************************************************************************************/

        /// <summary>Determines whether the <see cref="AnimancerState.MainObject"/> field can occupy the whole line.</summary>
        private bool IsAssetUsedAsKey
            => string.IsNullOrEmpty(Target.DebugName)
            && (Target.Key == null || ReferenceEquals(Target.Key, Target.MainObject));

        /************************************************************************************************************************/

        /// <inheritdoc/>
        protected override bool AutoNormalizeSiblingWeights
            => AutoNormalizeWeights;

        /************************************************************************************************************************/

        /// <summary>
        /// Draws the state's main label: an <see cref="Object"/> field if it has a
        /// <see cref="AnimancerState.MainObject"/>, otherwise just a simple text label.
        /// <para></para>
        /// Also shows a bar to indicate its progress.
        /// </summary>
        protected override void DoLabelGUI(Rect area)
        {
            string label;
            if (!string.IsNullOrEmpty(Target.DebugName))
            {
                label = Target.DebugName;
            }
            else if (IsAssetUsedAsKey)
            {
                label = "";
            }
            else
            {
                var key = Target.Key;
                if (key is string str)
                    label = $"\"{str}\"";
                else
                    label = key.ToString();
            }

            HandleLabelClick(area);

            AnimancerGUI.DoWeightLabel(ref area, Target.Weight);

            AnimationBindings.DoBindingMatchGUI(ref area, Target);

            var mainObject = Target.MainObject;
            if (!(mainObject is null))
            {
                EditorGUI.BeginChangeCheck();

                mainObject = EditorGUI.ObjectField(area, label, mainObject, typeof(Object), false);

                if (EditorGUI.EndChangeCheck())
                    Target.MainObject = mainObject;
            }
            else if (!string.IsNullOrEmpty(Target.DebugName))
            {
                EditorGUI.LabelField(area, Target.DebugName);
            }
            else
            {
                EditorGUI.LabelField(area, label, Target.ToString());
            }

            // Highlight a section of the label based on the time like a loading bar.
            area.width -= 18;// Remove the area for the Object Picker icon to line the bar up with the field.
            DoTimeHighlightBarGUI(area, Target.IsPlaying, Target.EffectiveWeight, Target.Time, Target.Length, Target.IsLooping);
        }

        /************************************************************************************************************************/

        /// <summary>Draws a progress bar to show the animation time.</summary>
        public static void DoTimeHighlightBarGUI(Rect area, bool isPlaying, float weight, float time, float length, bool isLooping)
        {
            var color = GUI.color;

            if (ScaleTimeBarByWeight)
            {
                var height = area.height;
                area.height = 1 + (area.height - 1) * Mathf.Clamp01(weight);
                area.y += height - area.height;
            }

            // Green = Playing, Yelow = Paused.
            GUI.color = isPlaying
                ? new Color(0.15f, 0.7f, 0.15f, 0.35f)
                : new Color(0.7f, 0.7f, 0.15f, 0.35f);

            area = EditorGUI.IndentedRect(area);

            var wrappedTime = GetWrappedTime(time, length, isLooping);
            if (length > 0)
                area.width *= Mathf.Clamp01(wrappedTime / length);

            GUI.DrawTexture(area, Texture2D.whiteTexture);

            GUI.color = color;
        }

        /************************************************************************************************************************/

        /// <summary>Handles Ctrl + Click on the label to CrossFade the animation.</summary>
        private void HandleLabelClick(Rect area)
        {
            var currentEvent = Event.current;
            if (currentEvent.type != EventType.MouseUp ||
                !currentEvent.control ||
                !area.Contains(currentEvent.mousePosition))
                return;

            currentEvent.Use();

            Target.Root.UnpauseGraph();
            var fadeDuration = Target.CalculateEditorFadeDuration(AnimancerPlayable.DefaultFadeDuration);
            Target.Root.Play(Target, fadeDuration);
        }

        /************************************************************************************************************************/

        /// <inheritdoc/>
        protected override void DoFoldoutGUI(Rect area)
        {
            float foldoutWidth;
            if (IsAssetUsedAsKey)
            {
                foldoutWidth = EditorGUI.indentLevel * AnimancerGUI.IndentSize;
            }
            else
            {
                foldoutWidth = EditorGUIUtility.labelWidth;
            }

            area.xMin -= 2;
            area.width = foldoutWidth;

            var hierarchyMode = EditorGUIUtility.hierarchyMode;
            EditorGUIUtility.hierarchyMode = true;

            IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);

            EditorGUIUtility.hierarchyMode = hierarchyMode;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Gets the current <see cref="AnimancerState.Time"/>.
        /// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
        /// </summary>
        private float GetWrappedTime(out float length) => GetWrappedTime(Target.Time, length = Target.Length, Target.IsLooping);

        /// <summary>
        /// Gets the current <see cref="AnimancerState.Time"/>.
        /// If the state is looping, the value is modulo by the <see cref="AnimancerState.Length"/>.
        /// </summary>
        private static float GetWrappedTime(float time, float length, bool isLooping)
        {
            var wrappedTime = time;

            if (isLooping)
            {
                wrappedTime = AnimancerUtilities.Wrap(wrappedTime, length);
                if (wrappedTime == 0 && time != 0)
                    wrappedTime = length;
            }

            return wrappedTime;
        }

        /************************************************************************************************************************/

        /// <inheritdoc/>
        protected override void DoDetailsGUI()
        {
            if (!IsExpanded)
                return;

            EditorGUI.indentLevel++;
            DoTimeSliderGUI();
            DoNodeDetailsGUI();
            DoOnEndGUI();
            EditorGUI.indentLevel--;
        }

        /************************************************************************************************************************/

        /// <summary>Draws a slider for controlling the current <see cref="AnimancerState.Time"/>.</summary>
        private void DoTimeSliderGUI()
        {
            if (Target.Length <= 0)
                return;

            var time = GetWrappedTime(out var length);

            if (length == 0)
                return;

            var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);

            var normalized = DoNormalizedTimeToggle(ref area);

            string label;
            float max;
            if (normalized)
            {
                label = "Normalized Time";
                time /= length;
                max = 1;
            }
            else
            {
                label = "Time";
                max = length;
            }

            DoLoopCounterGUI(ref area, length);

            EditorGUI.BeginChangeCheck();

            label = AnimancerGUI.BeginTightLabel(label);
            time = EditorGUI.Slider(area, label, time, 0, max);
            AnimancerGUI.EndTightLabel();

            if (AnimancerGUI.TryUseClickEvent(area, 2))
                time = 0;

            if (EditorGUI.EndChangeCheck())
            {
                if (normalized)
                    Target.NormalizedTime = time;
                else
                    Target.Time = time;
            }
        }

        /************************************************************************************************************************/

        private bool DoNormalizedTimeToggle(ref Rect area)
        {
            using (ObjectPool.Disposable.AcquireContent(out var label, "N"))
            {
                var style = AnimancerGUI.MiniButton;

                var width = style.CalculateWidth(label);
                var toggleArea = AnimancerGUI.StealFromRight(ref area, width);

                UseNormalizedTimeSliders.Value = GUI.Toggle(toggleArea, UseNormalizedTimeSliders, label, style);
            }

            return UseNormalizedTimeSliders;
        }

        /************************************************************************************************************************/

        private static ConversionCache<int, string> _LoopCounterCache;

        private void DoLoopCounterGUI(ref Rect area, float length)
        {
            if (_LoopCounterCache == null)
                _LoopCounterCache = new ConversionCache<int, string>((x) => "x" + x);

            string label;
            var normalizedTime = Target.Time / length;
            if (float.IsNaN(normalizedTime))
            {
                label = "NaN";
            }
            else
            {
                var loops = Mathf.FloorToInt(Target.Time / length);
                label = _LoopCounterCache.Convert(loops);
            }

            var width = AnimancerGUI.CalculateLabelWidth(label);

            var labelArea = AnimancerGUI.StealFromRight(ref area, width);

            GUI.Label(labelArea, label);
        }

        /************************************************************************************************************************/

        private void DoOnEndGUI()
        {
            if (!Target.HasEvents)
                return;

            var events = Target.Events;
            var drawer = EventSequenceDrawer.Get(events);
            var area = GUILayoutUtility.GetRect(0, drawer.CalculateHeight(events) + AnimancerGUI.StandardSpacing);
            area.yMin += AnimancerGUI.StandardSpacing;

            using (ObjectPool.Disposable.AcquireContent(out var label, "Events"))
                drawer.Draw(ref area, events, label);
        }

        /************************************************************************************************************************/
        #region Context Menu
        /************************************************************************************************************************/

        /// <inheritdoc/>
        protected override void PopulateContextMenu(GenericMenu menu)
        {
            AddContextMenuFunctions(menu);

            menu.AddFunction("Play",
                !Target.IsPlaying || Target.Weight != 1,
                () =>
                {
                    Target.Root.UnpauseGraph();
                    Target.Root.Play(Target);
                });

            AnimancerEditorUtilities.AddFadeFunction(menu, "Cross Fade (Ctrl + Click)",
                Target.Weight != 1,
                Target, (duration) =>
                {
                    Target.Root.UnpauseGraph();
                    Target.Root.Play(Target, duration);
                });

            menu.AddSeparator("");
            menu.AddItem(new GUIContent("Destroy State"), false, () => Target.Destroy());

            menu.AddSeparator("");

            AddDisplayOptions(menu);

            AnimancerEditorUtilities.AddDocumentationLink(menu, "State Documentation", Strings.DocsURLs.States);
        }

        /************************************************************************************************************************/

        /// <summary>Adds the details of this state to the `menu`.</summary>
        protected virtual void AddContextMenuFunctions(GenericMenu menu)
        {
            menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}{nameof(AnimancerState.Key)}: {Target.Key}"));

            var length = Target.Length;
            if (!float.IsNaN(length))
                menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}{nameof(AnimancerState.Length)}: {length}"));

            menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}Playable Path: {Target.GetPath()}"));

            var mainAsset = Target.MainObject;
            if (mainAsset != null)
            {
                var assetPath = AssetDatabase.GetAssetPath(mainAsset);
                if (assetPath != null)
                    menu.AddDisabledItem(new GUIContent($"{DetailsPrefix}Asset Path: {assetPath.Replace("/", "->")}"));
            }

            if (Target.HasEvents)
            {
                var events = Target.Events;
                for (int i = 0; i < events.Count; i++)
                {
                    var index = i;
                    AddEventFunctions(menu, "Event " + index, events[index],
                        () => events.SetCallback(index, AnimancerEvent.DummyCallback),
                        () => events.Remove(index));
                }

                AddEventFunctions(menu, "End Event", events.EndEvent,
                    () => events.EndEvent = new AnimancerEvent(float.NaN, null), null);
            }
        }

        /************************************************************************************************************************/

        private void AddEventFunctions(GenericMenu menu, string name, AnimancerEvent animancerEvent,
            GenericMenu.MenuFunction clearEvent, GenericMenu.MenuFunction removeEvent)
        {
            name = $"Events/{name}/";

            menu.AddDisabledItem(new GUIContent($"{name}{nameof(AnimancerState.NormalizedTime)}: {animancerEvent.normalizedTime}"));

            bool canInvoke;
            if (animancerEvent.callback == null)
            {
                menu.AddDisabledItem(new GUIContent(name + "Callback: null"));
                canInvoke = false;
            }
            else if (animancerEvent.callback == AnimancerEvent.DummyCallback)
            {
                menu.AddDisabledItem(new GUIContent(name + "Callback: Dummy"));
                canInvoke = false;
            }
            else
            {
                var label = name +
                    (animancerEvent.callback.Target != null ? ("Target: " + animancerEvent.callback.Target) : "Target: null");

                var targetObject = animancerEvent.callback.Target as Object;
                menu.AddFunction(label,
                    targetObject != null,
                    () => Selection.activeObject = targetObject);

                menu.AddDisabledItem(new GUIContent(
                    $"{name}Declaring Type: {animancerEvent.callback.Method.DeclaringType.GetNameCS()}"));

                menu.AddDisabledItem(new GUIContent(
                    $"{name}Method: {animancerEvent.callback.Method}"));

                canInvoke = true;
            }

            if (clearEvent != null)
                menu.AddFunction(name + "Clear", canInvoke || !float.IsNaN(animancerEvent.normalizedTime), clearEvent);

            if (removeEvent != null)
                menu.AddFunction(name + "Remove", true, removeEvent);

            menu.AddFunction(name + "Invoke", canInvoke, () => animancerEvent.Invoke(Target));
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
    }
}

#endif

